/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.bpm.engine.delegate.event.impl;

import com.je.bpm.core.model.FlowElement;
import com.je.bpm.core.model.FlowNode;
import com.je.bpm.engine.delegate.DelegateExecution;
import com.je.bpm.engine.delegate.event.*;
import com.je.bpm.engine.impl.bpmn.behavior.TerminateEndEventActivityBehavior;
import com.je.bpm.engine.impl.persistence.entity.ExecutionEntity;
import com.je.bpm.engine.impl.persistence.entity.IdentityLinkEntity;
import com.je.bpm.engine.impl.persistence.entity.VariableInstanceEntity;
import com.je.bpm.engine.impl.variable.VariableType;
import com.je.bpm.engine.repository.ProcessDefinition;
import com.je.bpm.engine.runtime.Job;
import com.je.bpm.engine.runtime.ProcessInstance;
import com.je.bpm.engine.task.Task;

import java.util.Map;

/**
 * Builder class used to create {@link ActivitiEvent} implementations.
 */
public class ActivitiEventBuilder {

    /**
     * @param type type of event
     * @return an {@link ActivitiEvent} that doesn't have it's execution context-fields filled, as the event is a global event, independent of any running execution.
     */
    public static ActivitiEvent createGlobalEvent(ActivitiEventType type) {
        ActivitiEventImpl newEvent = new ActivitiEventImpl(type);
        return newEvent;
    }

    public static ActivitiEvent createEvent(ActivitiEventType type, String executionId, String processInstanceId, String processDefinitionId) {
        ActivitiEventImpl newEvent = new ActivitiEventImpl(type);
        newEvent.setExecutionId(executionId);
        newEvent.setProcessDefinitionId(processDefinitionId);
        newEvent.setProcessInstanceId(processInstanceId);
        return newEvent;
    }

    /**
     * @param type   type of event
     * @param entity the entity this event targets
     * @return an {@link ActivitiEntityEvent}. In case an {@link com.je.bpm.engine.impl.context.ExecutionContext} is active, the execution related event fields will be populated. If not, execution details will be retrieved from the
     * {@link Object} if possible.
     */
    public static ActivitiEntityEvent createEntityEvent(ActivitiEventType type, Object entity) {
        ActivitiEntityEventImpl newEvent = new ActivitiEntityEventImpl(entity, type);

        // In case an execution-context is active, populate the event fields
        // related to the execution
        populateEventWithCurrentContext(newEvent);
        return newEvent;
    }

    /**
     * @param entity    the entity this event targets
     * @param variables the variables associated with this entity
     * @return an {@link ActivitiEntityEvent}. In case an {@link com.je.bpm.engine.impl.context.ExecutionContext} is active, the execution related
     * event fields will be populated. If not, execution details will be reteived from the {@link Object} if
     * possible.
     */
    @SuppressWarnings("rawtypes")
    public static ActivitiProcessStartedEvent createProcessStartedEvent(final Object entity,
                                                                        final Map variables, final boolean localScope) {
        final ActivitiProcessStartedEventImpl newEvent = new ActivitiProcessStartedEventImpl(entity, variables, localScope);

        // In case an execution-context is active, populate the event fields related to the execution
        populateEventWithCurrentContext(newEvent);
        return newEvent;
    }

    /**
     * @param type      type of event
     * @param entity    the entity this event targets
     * @param variables the variables associated with this entity
     * @return an {@link ActivitiEntityEvent}. In case an {@link com.je.bpm.engine.impl.context.ExecutionContext} is active, the execution related event fields will be populated. If not, execution details will be retrieved from the
     * {@link Object} if possible.
     */
    @SuppressWarnings("rawtypes")
    public static ActivitiEntityWithVariablesEvent createEntityWithVariablesEvent(ActivitiEventType type, Object entity, Map variables, boolean localScope) {
        ActivitiEntityWithVariablesEventImpl newEvent = new ActivitiEntityWithVariablesEventImpl(entity, variables, localScope, type);

        // In case an execution-context is active, populate the event fields
        // related to the execution
        populateEventWithCurrentContext(newEvent);
        return newEvent;
    }

    public static ActivitiSequenceFlowTakenEvent createSequenceFlowTakenEvent(ExecutionEntity executionEntity, ActivitiEventType type,
                                                                              String sequenceFlowId, String sourceActivityId, String sourceActivityName, String sourceActivityType, Object sourceActivityBehavior,
                                                                              String targetActivityId, String targetActivityName, String targetActivityType, Object targetActivityBehavior) {

        ActivitiSequenceFlowTakenEventImpl newEvent = new ActivitiSequenceFlowTakenEventImpl(type);

        if (executionEntity != null) {
            newEvent.setExecutionId(executionEntity.getId());
            newEvent.setProcessInstanceId(executionEntity.getProcessInstanceId());
            newEvent.setProcessDefinitionId(executionEntity.getProcessDefinitionId());
        }

        newEvent.setId(sequenceFlowId);
        newEvent.setSourceActivityId(sourceActivityId);
        newEvent.setSourceActivityName(sourceActivityName);
        newEvent.setSourceActivityType(sourceActivityType);
        newEvent.setSourceActivityBehaviorClass(sourceActivityBehavior != null ? sourceActivityBehavior.getClass().getCanonicalName() : null);
        newEvent.setTargetActivityId(targetActivityId);
        newEvent.setTargetActivityName(targetActivityName);
        newEvent.setTargetActivityType(targetActivityType);
        newEvent.setTargetActivityBehaviorClass(targetActivityBehavior != null ? targetActivityBehavior.getClass().getCanonicalName() : null);

        return newEvent;
    }

    /**
     * @param type   type of event
     * @param entity the entity this event targets
     * @return an {@link ActivitiEntityEvent}
     */
    public static ActivitiEntityEvent createEntityEvent(ActivitiEventType type, Object entity, String executionId, String processInstanceId, String processDefinitionId) {
        ActivitiEntityEventImpl newEvent = new ActivitiEntityEventImpl(entity, type);

        newEvent.setExecutionId(executionId);
        newEvent.setProcessInstanceId(processInstanceId);
        newEvent.setProcessDefinitionId(processDefinitionId);
        return newEvent;
    }

    /**
     * @param type   type of event
     * @param entity the entity this event targets
     * @param cause  the cause of the event
     * @return an {@link ActivitiEntityEvent} that is also instance of {@link ActivitiExceptionEvent}. In case an {@link com.je.bpm.engine.impl.context.ExecutionContext} is active, the execution related event fields will be
     * populated.
     */
    public static ActivitiEntityEvent createEntityExceptionEvent(ActivitiEventType type, Object entity, Throwable cause) {
        ActivitiEntityExceptionEventImpl newEvent = new ActivitiEntityExceptionEventImpl(entity, type, cause);

        // In case an execution-context is active, populate the event fields
        // related to the execution
        populateEventWithCurrentContext(newEvent);
        return newEvent;
    }

    /**
     * @param type   type of event
     * @param entity the entity this event targets
     * @param cause  the cause of the event
     * @return an {@link ActivitiEntityEvent} that is also instance of {@link ActivitiExceptionEvent}.
     */
    public static ActivitiEntityEvent createEntityExceptionEvent(ActivitiEventType type, Object entity, Throwable cause, String executionId, String processInstanceId, String processDefinitionId) {
        ActivitiEntityExceptionEventImpl newEvent = new ActivitiEntityExceptionEventImpl(entity, type, cause);

        newEvent.setExecutionId(executionId);
        newEvent.setProcessInstanceId(processInstanceId);
        newEvent.setProcessDefinitionId(processDefinitionId);
        return newEvent;
    }

    public static ActivitiActivityEvent createActivityEvent(ActivitiEventType type, String activityId, String activityName, String executionId,
                                                            String processInstanceId, String processDefinitionId, FlowElement flowElement) {

        ActivitiActivityEventImpl newEvent = new ActivitiActivityEventImpl(type);
        newEvent.setActivityId(activityId);
        newEvent.setActivityName(activityName);
        newEvent.setExecutionId(executionId);
        newEvent.setProcessDefinitionId(processDefinitionId);
        newEvent.setProcessInstanceId(processInstanceId);

        if (flowElement instanceof FlowNode) {
            FlowNode flowNode = (FlowNode) flowElement;
            newEvent.setActivityType(parseActivityType(flowNode));
            newEvent.setBehaviorClass(parseActivityBehavior(flowNode));
        }

        return newEvent;
    }

    protected static String parseActivityType(FlowNode flowNode) {
        String elementType = flowNode.getClass().getSimpleName();
        elementType = elementType.substring(0, 1).toLowerCase() + elementType.substring(1);
        return elementType;
    }

    protected static String parseActivityBehavior(FlowNode flowNode) {
        Object behaviour = flowNode.getBehavior();
        if (behaviour != null) {
            return (behaviour.getClass().getCanonicalName());
        }
        return null;
    }

    public static ActivitiActivityCancelledEvent createActivityCancelledEvent(String activityId, String activityName, String executionId,
                                                                              String processInstanceId, String processDefinitionId, String activityType, Object cause) {

        ActivitiActivityCancelledEventImpl newEvent = new ActivitiActivityCancelledEventImpl();
        newEvent.setActivityId(activityId);
        newEvent.setActivityName(activityName);
        newEvent.setExecutionId(executionId);
        newEvent.setProcessDefinitionId(processDefinitionId);
        newEvent.setProcessInstanceId(processInstanceId);
        newEvent.setActivityType(activityType);
        newEvent.setCause(cause);
        return newEvent;
    }

    public static ActivitiActivityCancelledEvent createActivityCancelledEvent(ExecutionEntity execution,
                                                                              Object cause) {
        FlowNode currentFlowNode = (FlowNode) execution.getCurrentFlowElement();

        ActivitiActivityCancelledEventImpl newEvent = new ActivitiActivityCancelledEventImpl();
        newEvent.setActivityId(execution.getActivityId());
        newEvent.setActivityName(currentFlowNode.getName());
        newEvent.setExecutionId(execution.getId());
        newEvent.setProcessDefinitionId(execution.getProcessDefinitionId());
        newEvent.setProcessInstanceId(execution.getProcessInstanceId());
        newEvent.setActivityType(parseActivityType(currentFlowNode));
        newEvent.setBehaviorClass(parseActivityBehavior(currentFlowNode));
        newEvent.setCause(cause);
        return newEvent;
    }

    public static ActivitiProcessCancelledEvent createProcessCancelledEvent(
            ProcessInstance processInstance, Object cause) {
        ActivitiProcessCancelledEventImpl newEvent = new ActivitiProcessCancelledEventImpl(processInstance);
        newEvent.setExecutionId(processInstance.getId());
        newEvent.setProcessDefinitionId(processInstance.getProcessDefinitionId());
        newEvent.setProcessInstanceId(processInstance.getProcessInstanceId());
        newEvent.setCause(cause);
        return newEvent;
    }

    public static ActivitiSignalEvent createActivitiySignalledEvent(DelegateExecution execution,
                                                                    String signalName,
                                                                    Object payload) {
        return createSignalEvent(ActivitiEventType.ACTIVITY_SIGNALED,
                execution,
                signalName,
                payload);
    }

    public static ActivitiMessageEvent createMessageReceivedEvent(DelegateExecution execution,
                                                                  String messageName,
                                                                  String correlationKey,
                                                                  Object payload) {
        return createMessageEvent(ActivitiEventType.ACTIVITY_MESSAGE_RECEIVED,
                execution,
                messageName,
                correlationKey,
                payload);
    }

    public static ActivitiMessageEvent createMessageWaitingEvent(DelegateExecution execution,
                                                                 String messageName,
                                                                 String correlationKey) {
        return createMessageEvent(ActivitiEventType.ACTIVITY_MESSAGE_WAITING,
                execution,
                messageName,
                correlationKey,
                null);
    }

    public static ActivitiMessageEvent createMessageSentEvent(DelegateExecution execution,
                                                              String messageName,
                                                              String correlationKey,
                                                              Object payload) {
        return createMessageEvent(ActivitiEventType.ACTIVITY_MESSAGE_SENT,
                execution,
                messageName,
                correlationKey,
                payload);
    }

    private static ActivitiMessageEvent createMessageEvent(ActivitiEventType type,
                                                           DelegateExecution execution,
                                                           String messageName,
                                                           String correlationKey,
                                                           Object payload) {
        ActivitiMessageEventImpl newEvent = new ActivitiMessageEventImpl(type);
        newEvent.setMessageName(messageName);
        newEvent.setMessageCorrelationKey(correlationKey);
        newEvent.setMessageData(payload);
        newEvent.setMessageBusinessKey(execution.getProcessInstanceBusinessKey());

        applyExecution(newEvent, execution);

        return newEvent;
    }

    public static ActivitiErrorEvent createErrorEvent(ActivitiEventType type, String activityId, String errorId, String errorCode,
                                                      String executionId, String processInstanceId, String processDefinitionId) {
        ActivitiErrorEventImpl newEvent = new ActivitiErrorEventImpl(type);
        newEvent.setActivityId(activityId);
        newEvent.setExecutionId(executionId);
        newEvent.setProcessDefinitionId(processDefinitionId);
        newEvent.setProcessInstanceId(processInstanceId);
        newEvent.setErrorId(errorId);
        newEvent.setErrorCode(errorCode);
        return newEvent;
    }

    public static ActivitiVariableEvent createVariableEvent(ActivitiEventType type, String variableName, Object variableValue, VariableType variableType, String taskId, String executionId,
                                                            String processInstanceId, String processDefinitionId) {
        ActivitiVariableEventImpl newEvent = new ActivitiVariableEventImpl(type);
        newEvent.setVariableName(variableName);
        newEvent.setVariableValue(variableValue);
        newEvent.setVariableType(variableType);
        newEvent.setTaskId(taskId);
        newEvent.setExecutionId(executionId);
        newEvent.setProcessDefinitionId(processDefinitionId);
        newEvent.setProcessInstanceId(processInstanceId);
        return newEvent;
    }

    public static ActivitiVariableUpdatedEventImpl createVariableUpdateEvent(VariableInstanceEntity variableInstance, Object previousValue,
                                                                             String processInstanceId, String processDefinitionId) {
        ActivitiVariableUpdatedEventImpl updateEvent = new ActivitiVariableUpdatedEventImpl();

        String variableName = variableInstance.getName();
        Object variableValue = variableInstance.getValue();
        String executionId = variableInstance.getExecutionId();
        String taskId = variableInstance.getTaskId();
        VariableType variableType = variableInstance.getType();

        updateEvent.setVariableName(variableName);
        updateEvent.setVariableValue(variableValue);
        updateEvent.setVariablePreviousValue(previousValue);
        updateEvent.setVariableType(variableType);
        updateEvent.setTaskId(taskId);
        updateEvent.setExecutionId(executionId);
        updateEvent.setProcessDefinitionId(processDefinitionId);
        updateEvent.setProcessInstanceId(processInstanceId);

        return updateEvent;
    }

    public static ActivitiMembershipEvent createMembershipEvent(ActivitiEventType type, String groupId, String userId) {
        ActivitiMembershipEventImpl newEvent = new ActivitiMembershipEventImpl(type);
        newEvent.setUserId(userId);
        newEvent.setGroupId(groupId);
        return newEvent;
    }

    protected static void populateEventWithCurrentContext(ActivitiEventImpl event) {
        if (event instanceof ActivitiEntityEvent) {
            Object persistedObject = ((ActivitiEntityEvent) event).getEntity();
            if (persistedObject instanceof Job) {
                event.setExecutionId(((Job) persistedObject).getExecutionId());
                event.setProcessInstanceId(((Job) persistedObject).getProcessInstanceId());
                event.setProcessDefinitionId(((Job) persistedObject).getProcessDefinitionId());
            } else if (persistedObject instanceof DelegateExecution) {
                event.setExecutionId(((DelegateExecution) persistedObject).getId());
                event.setProcessInstanceId(((DelegateExecution) persistedObject).getProcessInstanceId());
                event.setProcessDefinitionId(((DelegateExecution) persistedObject).getProcessDefinitionId());
            } else if (persistedObject instanceof IdentityLinkEntity) {
                IdentityLinkEntity idLink = (IdentityLinkEntity) persistedObject;
                if (idLink.getProcessDefinitionId() != null) {
                    event.setProcessDefinitionId(idLink.getProcessDefId());
                } else if (idLink.getProcessInstance() != null) {
                    event.setProcessDefinitionId(idLink.getProcessInstance().getProcessDefinitionId());
                    event.setProcessInstanceId(idLink.getProcessInstanceId());
                    event.setExecutionId(idLink.getProcessInstanceId());
                } else if (idLink.getTask() != null) {
                    event.setProcessDefinitionId(idLink.getTask().getProcessDefinitionId());
                    event.setProcessInstanceId(idLink.getTask().getProcessInstanceId());
                    event.setExecutionId(idLink.getTask().getExecutionId());
                }
            } else if (persistedObject instanceof Task) {
                event.setProcessInstanceId(((Task) persistedObject).getProcessInstanceId());
                event.setExecutionId(((Task) persistedObject).getExecutionId());
                event.setProcessDefinitionId(((Task) persistedObject).getProcessDefinitionId());
                event.setReason(TerminateEndEventActivityBehavior.createDeleteReason(null));
            } else if (persistedObject instanceof ProcessDefinition) {
                event.setProcessDefinitionId(((ProcessDefinition) persistedObject).getId());
            }
        }
    }

    private static ActivitiSignalEvent createSignalEvent(ActivitiEventType type,
                                                         DelegateExecution execution,
                                                         String signalName,
                                                         Object payload) {
        ActivitiSignalEventImpl newEvent = new ActivitiSignalEventImpl(type);
        newEvent.setSignalName(signalName);
        newEvent.setSignalData(payload);

        applyExecution(newEvent, execution);

        return newEvent;
    }

    private static void applyExecution(ActivitiActivityEventImpl newEvent,
                                       DelegateExecution execution) {
        if (execution != null) {
            newEvent.setActivityId(execution.getCurrentActivityId());
            newEvent.setExecutionId(execution.getId());
            newEvent.setProcessDefinitionId(execution.getProcessDefinitionId());
            newEvent.setProcessInstanceId(execution.getProcessInstanceId());

            if (execution.getCurrentFlowElement() instanceof FlowNode) {
                FlowNode flowNode = (FlowNode) execution.getCurrentFlowElement();
                newEvent.setActivityType(parseActivityType(flowNode));
                newEvent.setBehaviorClass(parseActivityBehavior(flowNode));
                newEvent.setActivityName(flowNode.getName());
            }
        }
    }

}
